<?php
use frontend\modules\yii2\widgets\layouts\markdown\Markdown;

$this->title = 'Yii的控制器（Controller）使用教程';
$this->registerMetaTag([
    'name' => 'keywords',
    'content' => 'yii,yii2,框架,应用结构,controller,控制器,使用,教程'
]);
$this->registerMetaTag([
    'name' => 'description',
    'content' => '对Yii2框架的控制器（Controller）进行详细的介绍'
]);
$this->registerMetaTag([
    'name' => 'author',
    'content' => 'chenzhiwei'
]);

$html = [
    "概述" => <<<HTML
# Yii框架控制器（Controller）使用教程
控制器是 `MVC` 模式中的一部分， 继承 [yii\base\Controller](https://www.yiichina.com/doc/api/2.0/yii-base-controller) 类的对象，是负责处理请求和生成响应的主要场所，也是你业务逻辑的处理场所。 
HTML,
    "控制器的分类" => <<<HTML
## 控制器的分类
根据场景不同，控制器主要分为两类：
- Web控制器（[yii\web\Controller](https://www.yiichina.com/doc/api/2.0/yii-web-controller)）：接收并处理页面请求，主要以浏览器作为客户端；
- Console控制器（[yii\console\Controller](https://www.yiichina.com/doc/api/2.0/yii-console-controller)）：处理控制台任务，即：服务器本地处理的任务；
- Api控制器（[yii\\rest\Controller](https://www.yiichina.com/doc/api/2.0/yii-rest-controller)）：处理API请求，如：处理安卓、IOS等客户端发起的请求；

对于刚接触PHP不久的同学可能不太理解什么是`控制台任务`，其实就是在服务端本地执行的PHP代码段，比如说：你要做一个股票分析的网站，需要每天定时从股票网站爬取当天所有股票信息，此时就应该创建一个控制台类型的控制器，在这个控制器中写脚本拉取股票数据，由于控制台类型的控制器是可以通过命令行触发执行的，因此只要在Linux的任务管理器`Crontab`中纳入定时任务即可自动完成每天数据爬取工作。

HTML,
    "创建控制器" => <<<HTML
## 创建控制器
创建控制器有如下两种方式：
- 手撕代码
- Gii创建新控制器

Gii是Yii提供的一种可视化页面操作的代码生成器，可以帮我们完成快速开发。

### 手撕代码
这种方式很简单，你只需要复制现有的控制器文件并修改一下文件名称和内容即可。

举个例子，如果你想创建一个`article`控制器，用于处理文章的创建/修改/查看请求，你就可以复制`frontend/controllers/SiteController.php`这个已存在的控制器文件，步骤如下：
1. 复制文件到目标目录下（这里放到`SiteController.php`同层目录）；
2. 修改文件名为：`ArticleController.php`；
3. 打开文件，修改类名称为：`ArticleController`；
4. 删除原文件中的类方法；
5. 创建相关视图目录和文件；

修改完成后的文件`frontend/controllers/ArticleController.php`如下所示：
```php
namespace frontend\controllers;

use Yii;
use yii\web\Controller;

class ArticleController extends Controller
{
    
}
```

### Gii创建法 id=gii-creation
Gii代码生成器可以帮我们完成控制器的创建工作，但是需要我们在配置文件中开通Gii的访问权限：
```php
if(YII_ENV_DEV){
    \$config['bootstrap'][] = 'gii';
    \$config['modules']['gii'] = [
        'class' => 'yii\gii\Module',
        'allowedIPs' => ['*']
    ]; 
}
```
> 注意：在Yii默认提供的框架示例代码中是没有`allowdIPs`选项的，因此默认不允许使用Gii，`*`表示允许所有IP进行Gii操作。

然后访问`http://hostname/gii`即可：
![Gii控制台页面](/images/20211209150758.png "Gii控制台页面")

点击第三个`Controller Generator`，进入控制器生成器页面，并填好新控制器相关信息：
![Gii控制台-控制器创建页面](/images/20211209151334.png "Gii控制台-控制器创建页面")

点击"Preview"按钮，会出现即将创建的文件列表：
![Gii控制台-控制器创建页面-Preview](/images/20211209151508.png "Gii控制台-控制器创建页面-Preview")

确保无误后，点击"Generate"按钮就会完成文件和目录（视图目录）的创建：
```php
namespace frontend\controllers;

class ArticleController extends \yii\web\Controller
{
    public function actionIndex()
    {
        return \$this->render('index');
    }
}
```
Gii创建法的好处就是它可以连同视图文件和目录一起创建，出错率低。

HTML,
    "controllerMap" => <<<HTML
## 控制器映射（controllerMap）
Yii的路由机制会根据输入的控制器名称来找到同名的控制器，比如说：
```php
// 默认路由机制如下：
'http://hostname/site/contact' => 'frontend\controllers\SiteController'
```
但是控制器映射可以让我们改变这种默认规则，举个例子：
```php
// 配置controllerMap的路由结果
'http://hostname/home/contact' => 'frontend\controllers\SiteController'
```
此时不再是`site`对应`SiteController`，而是`home`对应`SiteController`。要完成这种映射的配置，需要在配置文件中定义映射关系：
```php
return [
    'controllerNamespace' => 'frontend\controllers',
    'controllerMap' => [
        // 用配置数组申明 "home" 控制器
        'home' => [
            'class' => 'frontend\controllers\SiteController',
            //'enableCsrfValidation' => false,
        ],
    ]
];
```
> 注意：`controllerMap`中的`class`命名空间需要和`controllerNamespace`中定义的保持一至。

如果你想映射到`modules`模块里的控制器，就需要在模块中定义：
```php
return [
    'modules' => [
        'yii2' => [
            'class' => 'frontend\modules\yii2\Module',
            
            // 在此处定义模块内的控制器映射关系
            'controllerMap' => [
                'home' => [
                    'class' => 'frontend\modules\yii2\controllers\SiteController',
                    //'enableCsrfValidation' => false,
                ],
            ]
        ],
    ],
];
```

> 注意：控制器映射会改变控制器的id，因此在渲染视图时，会去找以新控制器id为目录的视图文件，如上面配置，会默认去找`views/home/contact.php`这个视图文件。

HTML,
    "name" => <<<HTML
## 路由步骤
Yii在解析URL的时候遵循如下步骤找到对应的控制器，以`http://localhost/admin/post-comment`为例：
- 第一步，去掉中横杠，把横杠前后单词首字母大写后连起来：
```php
'/admin/post-comment' => '/admin/PostComment'
```
- 第二步，增加Controller后缀：
```php
'/admin/PostComment' => '/admin/PostCommentController'
```
- 第三步，将用正斜杠区分的每个单词第一个字母转为大写。注意如果包含多个正斜杠，只将最后的正斜杠后的部分第一个字母转为大写：
```php
'/admin/PostCommentController' => '/admin/PostCommentController'
```
- 最后，在前面增加控制器命名空间，将正斜杠替换为反斜杠：
```php
'/admin/PostCommentController' => 'frontend\controllers\admin\PostCommentController'
```

到此得到此URL对应的控制器：`frontend\controllers\admin\PostCommentController`。

> 敲黑板：由上述最后一条可知，控制器内定义的命名空间（namespace）承担着Yii寻找控制器文件所在位置的作用，因此需要和真实路径保持一致。

HTML,
    "action" => <<<HTML
## 动作（action）
控制器（Controller）中的动作其实就是控制器类中以`action`为前缀的方法，举个例子：
```php
namespace app\controllers;

use yii\web\Controller;

class SiteController extends Controller
{
    public function actionIndex()
    {
        return \$this->render('index');
    }

    public function actionHelloWorld()
    {
        return 'Hello ' . \$this->printName;
    }
    
    private function printName(){
        return "Yii";
    }
}
```
上述代码中就有两个动作：`index`和`hello-world`，也只有这两个动作可以接收并处理web请求，`printName()`方法并没有以`action`作为前缀，因此Yii不会认为他是一个动作，它也无法接收请求，只能当做普通的类方法被调用；

HTML,
    "beforeAction" => <<<HTML
### 预执行（beforeAction）
在执行任何一个动作前，控制器都会调用`beforeAction()`这个方法，这可以让我们做很多事，比如：数据校验工作。
```php
class SiteController extends Controller
{
    public function actionIndex()
    {
        return \$this->render('index');
    }

    public function actionHelloWorld()
    {
        return 'Hello World';
    }

    public function beforeAction(\$action)
    {
        \$time = date("Y-m-d H:i:s");
        switch(\$action->id){
            case "index":
                // 此处编写index动作的预处理脚本
                file_put_contents("/tmp/visit.log", \$time . " : " . "index". PHP_EOL, 8);
                break;
            case "hello-world":
                // 此处编写hello-world动作的预处理脚本
                file_put_contents("/tmp/visit.log", \$time . " : " . "hello-world", 8);
                break;
            default:
                break;
        }

        return parent::beforeAction(\$action);
    }
}
```

HTML,
    "after" => <<<HTML
### 后处理（afterAction）
这是在执行目标动作后都会调用的一个方法，大家可以在这个方法里对动作中返回的结果进行统一的处理，比如说：格式化输出。
```php
class SiteController extends Controller
{
    public function actionIndex()
    {
        return \$this->render('index');
    }

    public function actionHelloWorld()
    {
        return 'Hello World';
    }

    public function afterAction(\$action, \$result)
    {
        \$result = json_encode(\$result);
        return parent::afterAction(\$action, \$result);
    }

}
```

HTML,
    "extra" => <<<HTML
### 独立动作 id=extra-action
独立动作就是指可以被多个控制器调用的动作，它继承 [yii\base\Action](https://www.yiichina.com/doc/api/2.0/yii-base-action) 或它的子类来定义，如下：
```php
namespace frontend\actions;

use yii\base\Action;

class HelloAction extends Action
{
    public function run()
    {
        return "Hello World";
    }
}
```
使用方法很简单，在目标控制器的`actions()`方法中引用即可：
```php
namespace frontend\controllers;

use yii\web\Controller;

class SiteController extends Controller
{
    public function actions()
    {
        return [
            'hello' => [
                'class' => 'frontend\actions\HelloAction'
            ]
        ];
    }
}
```
上述`SiteController`控制器中引用了`HelloAction`动作，因此您可以通过`site/hello`即可输出"Hello World"。

HTML,

    "params" => <<<HTML
### 动作参数
动作方法可以设置参数，参数的值来自于`\$_GET`，举例如下：
```php
namespace frontend\controllers;

use yii\web\Controller;

class SiteController extends Controller
{
    // http://hostname/site/login?username=lvzmen&password=123456
    public function actionLogin(\$username, \$password, \$remember = 1)
    {
        
    }
}
```
在访问上述动作的时候，你的url必须包含`\$username`和`\$password`参数，否则会抛出异常，由于`\$remember`有默认值，所以允许不设置。这种方法提供了一种弱校验机制，校验了URL的书写规范，非常便于处理用`GET`方法提交的数据。


HTML,

];




echo Markdown::widget(["content" => implode("\n", $html)]);

